use super::*;
use std::fs;
use std::io::Write;
use std::path::PathBuf;

/// Helper function to create a file with content.
fn create_file(path: &str, content: &str) -> PathBuf {
    let path = PathBuf::from(path);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    let mut file = fs::File::create(&path).unwrap();
    file.write_all(content.as_bytes()).unwrap();
    path
}

#[tokio::test]
#[serial]
/// Tests the basic remove functionality by removing a single file
async fn test_remove_single_file() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a file and add it to index
    let file_path = create_file("test_file.txt", "Test content");

    add::execute(AddArgs {
        pathspec: vec![String::from("test_file.txt")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Make sure the file exists
    assert!(file_path.exists(), "File should exist before removal");

    // Remove the file
    let args = RemoveArgs {
        pathspec: vec![String::from("test_file.txt")],
        cached: false,
        recursive: false,
        force: false,
        dry_run: false,
    };
    remove::execute(args).unwrap();

    // Verify the file was removed from the filesystem
    assert!(
        !file_path.exists(),
        "File should be removed from filesystem"
    );

    // Verify file is no longer in the index
    let changes = changes_to_be_staged();
    assert!(
        !changes
            .new
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should not appear in changes as new"
    );
    assert!(
        !changes
            .modified
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should not appear in changes as modified"
    );
    assert!(
        !changes
            .deleted
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should not appear in changes as deleted"
    );
}

#[tokio::test]
#[serial]
/// Tests removing a file with --cached flag, which only removes from the index but keeps the file
async fn test_remove_cached() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a file and add it to index
    let file_path = create_file("test_file.txt", "Test content");

    add::execute(AddArgs {
        pathspec: vec![String::from("test_file.txt")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Make sure the file exists
    assert!(file_path.exists(), "File should exist before removal");

    // Remove the file with --cached flag
    let args = RemoveArgs {
        pathspec: vec![String::from("test_file.txt")],
        cached: true,
        recursive: false,
        force: false,
        dry_run: false,
    };
    remove::execute(args).unwrap();

    // Verify the file still exists in the filesystem
    assert!(file_path.exists(), "File should still exist in filesystem");

    // Verify file appears as new (untracked) in the index
    let changes = changes_to_be_staged();
    assert!(
        changes
            .new
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should appear in changes as new/untracked"
    );
}

#[tokio::test]
#[serial]
/// Tests recursive removal of a directory
async fn test_remove_directory_recursive() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a directory with files
    let file1 = create_file("test_dir/file1.txt", "File 1 content");
    let file2 = create_file("test_dir/file2.txt", "File 2 content");
    let file3 = create_file("test_dir/subdir/file3.txt", "File 3 content");

    // Add all files to the index
    add::execute(AddArgs {
        pathspec: vec![String::from("test_dir")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Make sure the directory and files exist
    assert!(fs::metadata("test_dir").is_ok(), "Directory should exist");
    assert!(file1.exists(), "File 1 should exist");
    assert!(file2.exists(), "File 2 should exist");
    assert!(file3.exists(), "File 3 should exist");

    // Remove the directory recursively
    let args = RemoveArgs {
        pathspec: vec![String::from("test_dir")],
        cached: false,
        recursive: true,
        force: false,
        dry_run: false,
    };
    remove::execute(args).unwrap();

    // Verify the directory and files were removed
    assert!(
        fs::metadata("test_dir").is_err(),
        "Directory should be removed"
    );
    assert!(!file1.exists(), "File 1 should be removed");
    assert!(!file2.exists(), "File 2 should be removed");
    assert!(!file3.exists(), "File 3 should be removed");

    // Verify files are no longer in the index
    let changes = changes_to_be_staged();
    for file in &[file1, file2, file3] {
        let file_str = file.to_str().unwrap();
        assert!(
            !changes.new.iter().any(|x| x.to_str().unwrap() == file_str),
            "File should not appear in changes as new"
        );
        assert!(
            !changes
                .modified
                .iter()
                .any(|x| x.to_str().unwrap() == file_str),
            "File should not appear in changes as modified"
        );
        assert!(
            !changes
                .deleted
                .iter()
                .any(|x| x.to_str().unwrap() == file_str),
            "File should not appear in changes as deleted"
        );
    }
}

#[tokio::test]
#[serial]
/// Tests attempting to remove a directory without -r flag should fail
async fn test_remove_directory_without_recursive() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a directory with files
    let file1 = create_file("test_dir/file1.txt", "File 1 content");
    let file2 = create_file("test_dir/file2.txt", "File 2 content");

    // Add all files to the index
    add::execute(AddArgs {
        pathspec: vec![String::from("test_dir")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Make sure the directory and files exist
    assert!(fs::metadata("test_dir").is_ok(), "Directory should exist");
    assert!(file1.exists(), "File 1 should exist");
    assert!(file2.exists(), "File 2 should exist");

    // Attempt to remove the directory without recursive flag
    let args = RemoveArgs {
        pathspec: vec![String::from("test_dir")],
        cached: false,
        recursive: false,
        force: false,
        dry_run: false,
    };
    assert!(
        remove::execute(args).is_err(),
        "Removing a directory without recursive should fail"
    );

    // Verify the directory and files still exist
    assert!(
        fs::metadata("test_dir").is_ok(),
        "Directory should still exist"
    );
    assert!(file1.exists(), "File 1 should still exist");
    assert!(file2.exists(), "File 2 should still exist");
}

#[tokio::test]
#[serial]
/// Tests removing a file that does not exist in the index
async fn test_remove_untracked_file() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a file but don't add it to the index
    let file_path = create_file("untracked_file.txt", "Untracked content");

    // Make sure the file exists
    assert!(file_path.exists(), "File should exist");

    // Attempt to remove the untracked file (should fail/do nothing)
    let args = RemoveArgs {
        pathspec: vec![String::from("untracked_file.txt")],
        cached: false,
        recursive: false,
        force: false,
        dry_run: false,
    };
    let result = remove::execute(args);
    assert!(
        result.is_err(),
        "Removing an untracked file should return an error, not panic"
    );

    // Verify the file still exists
    assert!(file_path.exists(), "File should still exist");
}

#[tokio::test]
#[serial]
/// Tests removing a file that has been modified after being added to the index
async fn test_remove_modified_file() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a file and add it to index
    let file_path = create_file("test_file.txt", "Initial content");

    add::execute(AddArgs {
        pathspec: vec![String::from("test_file.txt")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Modify the file
    let mut file = fs::OpenOptions::new()
        .write(true)
        .truncate(true)
        .open(&file_path)
        .unwrap();
    file.write_all(b" - Modified").unwrap();

    // Remove the file
    let args = RemoveArgs {
        pathspec: vec![String::from("test_file.txt")],
        cached: false,
        recursive: false,
        force: false,
        dry_run: false,
    };
    remove::execute(args).unwrap();

    // Verify the file was removed.
    assert!(!file_path.exists(), "File should be removed");

    // Verify file is not in the index.
    let changes = changes_to_be_staged();
    assert!(
        !changes
            .new
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should not appear in changes as new"
    );
    assert!(
        !changes
            .modified
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should not appear in changes as modified"
    );
    assert!(
        !changes
            .deleted
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should not appear in changes as deleted"
    );
}

#[tokio::test]
#[serial]
/// Tests removing multiple files at once
async fn test_remove_multiple_files() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create multiple files
    let file1 = create_file("file1.txt", "File 1 content");
    let file2 = create_file("file2.txt", "File 2 content");
    let file3 = create_file("file3.txt", "File 3 content");

    // Add all files to the index
    add::execute(AddArgs {
        pathspec: vec![String::from(".")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Make sure all files exist
    assert!(file1.exists(), "File 1 should exist");
    assert!(file2.exists(), "File 2 should exist");
    assert!(file3.exists(), "File 3 should exist");

    // Remove multiple files at once
    let args = RemoveArgs {
        pathspec: vec![String::from("file1.txt"), String::from("file3.txt")],
        cached: false,
        recursive: false,
        force: false,
        dry_run: false,
    };
    remove::execute(args).unwrap();
    // Verify the specified files were removed
    assert!(!file1.exists(), "File 1 should be removed");
    assert!(file2.exists(), "File 2 should still exist");
    assert!(!file3.exists(), "File 3 should be removed");
}

#[tokio::test]
#[serial]
/// Tests the --dry-run flag which shows what would be removed without actually removing anything
async fn test_remove_dry_run() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create multiple files
    let file1 = create_file("file1.txt", "File 1 content");
    let file2 = create_file("file2.txt", "File 2 content");
    let file3 = create_file("subdir/file3.txt", "File 3 content");

    // Add all files to the index
    add::execute(AddArgs {
        pathspec: vec![String::from(".")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Make sure all files exist before dry-run
    assert!(file1.exists(), "File 1 should exist before dry-run");
    assert!(file2.exists(), "File 2 should exist before dry-run");
    assert!(file3.exists(), "File 3 should exist before dry-run");

    // Run rm with --dry-run flag
    let args = RemoveArgs {
        pathspec: vec![String::from("file1.txt"), String::from("file2.txt")],
        cached: false,
        recursive: false,
        force: false,
        dry_run: true,
    };
    remove::execute(args).unwrap();

    // Verify that no files were actually removed
    assert!(file1.exists(), "File 1 should still exist after dry-run");
    assert!(file2.exists(), "File 2 should still exist after dry-run");
    assert!(file3.exists(), "File 3 should still exist after dry-run");

    // Verify files are still in the index by checking they don't appear as deleted
    let changes = changes_to_be_staged();
    assert!(
        !changes
            .deleted
            .iter()
            .any(|x| x.to_str().unwrap() == "file1.txt"),
        "File 1 should not appear as deleted"
    );
    assert!(
        !changes
            .deleted
            .iter()
            .any(|x| x.to_str().unwrap() == "file2.txt"),
        "File 2 should not appear as deleted"
    );
}

#[tokio::test]
#[serial]
/// Tests --dry-run with --cached flag
async fn test_remove_dry_run_cached() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a file and add it to index
    let file_path = create_file("test_file.txt", "Test content");

    add::execute(AddArgs {
        pathspec: vec![String::from("test_file.txt")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Run rm with --dry-run and --cached flags
    let args = RemoveArgs {
        pathspec: vec![String::from("test_file.txt")],
        cached: true,
        recursive: false,
        force: false,
        dry_run: true,
    };
    remove::execute(args).unwrap();

    // Verify the file still exists in both filesystem and index
    assert!(file_path.exists(), "File should still exist in filesystem");

    // Verify file doesn't appear as deleted in changes
    let changes = changes_to_be_staged();
    assert!(
        !changes
            .deleted
            .iter()
            .any(|x| x.to_str().unwrap() == "test_file.txt"),
        "File should not appear as deleted"
    );
}

#[tokio::test]
#[serial]
/// Tests --dry-run with recursive directory removal
async fn test_remove_dry_run_recursive() {
    let test_dir = tempdir().unwrap();
    test::setup_with_new_libra_in(test_dir.path()).await;
    let _guard = test::ChangeDirGuard::new(test_dir.path());

    // Create a directory with files
    let file1 = create_file("test_dir/file1.txt", "File 1 content");
    let file2 = create_file("test_dir/file2.txt", "File 2 content");
    let file3 = create_file("test_dir/subdir/file3.txt", "File 3 content");

    // Add all files to the index
    add::execute(AddArgs {
        pathspec: vec![String::from("test_dir")],
        all: false,
        update: false,
        refresh: false,
        verbose: false,
        dry_run: false,
        ignore_errors: false,
    })
    .await;

    // Run rm with --dry-run and --recursive flags
    let args = RemoveArgs {
        pathspec: vec![String::from("test_dir")],
        cached: false,
        recursive: true,
        force: false,
        dry_run: true,
    };
    remove::execute(args).unwrap();

    // Verify that no files or directories were actually removed
    assert!(file1.exists(), "File 1 should still exist after dry-run");
    assert!(file2.exists(), "File 2 should still exist after dry-run");
    assert!(file3.exists(), "File 3 should still exist after dry-run");
    assert!(
        PathBuf::from("test_dir").exists(),
        "Directory should still exist"
    );
    assert!(
        PathBuf::from("test_dir/subdir").exists(),
        "Subdirectory should still exist"
    );

    // Verify files are still tracked by checking they don't appear as deleted
    let changes = changes_to_be_staged();
    assert!(
        !changes
            .deleted
            .iter()
            .any(|x| x.to_str().unwrap().starts_with("test_dir/")),
        "No files in test_dir should appear as deleted"
    );
}
